using System;
using System.Collections.Generic;
using System.Linq;
using Godot;

namespace Arcane.Source.TileMatch;

public class Match
{
    #region Events
    public event Action<ITile, TilePos> OnTileUpdate;
    public event Action<ITile, TilePos> OnTileRemoved;
    #endregion

    private ITileRespawner _respawner;

    private readonly ITile[,] _tiles;
    private readonly bool[] _dirtyTiles;

    private readonly List<Schedule> _schedules = [];
    private readonly List<Schedule> _scheduleBuffer = [];
    private readonly List<TilePos> _markedTiles = [];

    private bool prevTrig = false;

    private (TilePos, TilePos)? _hints = null;
    public (TilePos, TilePos)? Hints => _hints;

    public Match(int width, int height, ITileRespawner respawner)
    {
        _tiles = new ITile[width, height];
        _dirtyTiles = new bool[width * height];
        _respawner = respawner;
    }

    public int Width => _tiles.GetLength(0);
    public int Height => _tiles.GetLength(1);

    // public void AddListener(IMatchListener listener)
    // {
    //     _listeners.Add(listener);
    // }

    public void Init()
    {
        for (int row = 0; row < Height; row++)
        {
            for (int col = 0; col < Width; col++)
            {
                var tile = _respawner.Respawn(this);
                _tiles[col, row] = tile;
            }
        }
    }

    public void Tick()
    {
        Array.Fill(_dirtyTiles, false);

        foreach (var schedule in _schedules)
        {
            schedule.tick -= 1;
            if (schedule.tick <= 0)
            {
                schedule.Act();
            }
        }

        _schedules.RemoveAll(s => s.tick <= 0);

        bool trigMatch = true;
        for (int row = Height - 1; row >= 0; row--)
        {
            for (int col = 0; col < Width; col++)
            {
                if (_tiles[col, row] is IDynTile dynTile)
                {
                    if (dynTile.Tick(this, (col, row)))
                    {
                        _hints = null;
                        trigMatch = false;
                    }
                }
            }
        }

        if (trigMatch && !prevTrig)
        {
            ScheduleTick(() =>
            {
                DoMatch();
                while (!FindHint())
                {
                    Reshuffle();
                }
            }, 2);
        }

        for (var r = 0; r < Height; r++)
        {
            for (var c = 0; c < Width; c++)
            {
                if (_dirtyTiles[r * Width + c])
                {
                    OnTileUpdate?.Invoke(_tiles[c, r], (c, r));
                }
            }
        }

        // Post tick
        prevTrig = trigMatch;

        foreach (var schedule in _scheduleBuffer)
        {
            if (schedule.tick <= 0) schedule.Act();
        }
        _scheduleBuffer.RemoveAll(s => s.tick <= 0);
        _schedules.AddRange(_scheduleBuffer);
        _scheduleBuffer.Clear();
    }

    private void DoMatch()
    {
        // GD.Print("Matching..");
        List<TilePos> tilesBuffer = [];

        foreach (var tile in Enumerable.Reverse(_markedTiles))
        {
            tilesBuffer.Clear();
            if (ThreeMatch(tile, tilesBuffer))
            {
                var matchPos = tilesBuffer[^1];
                GetTileUnchecked(matchPos).OnMatch(this, matchPos, tilesBuffer);
            }
            else
            {
                // _listeners.ForEach(l => l.OnCellUpdate(GetTile(tile), tile));
                // OnTileUpdate?.Invoke(GetTile(tile), tile);
                MarkDirty(tile);
            }
        }

        _markedTiles.Clear();
    }

    private bool FindHint()
    {
        if (_hints is var (a, b))
        {
            SwapTileUnchecked(a, b);
            if (ThreeMatch(a) || ThreeMatch(b))
            {
                SwapTileUnchecked(a, b);
                _hints = (a, b);
                return true;
            }
            SwapTileUnchecked(a, b);
        }


        for (int row = Height - 1; row >= 0; row--)
        {
            for (int col = 0; col < Width; col++)
            {
                TilePos pos = (col, row);

                if (IsPosValid(pos.Right()))
                {
                    SwapTileUnchecked(pos, pos.Right());
                    if (ThreeMatch(pos) || ThreeMatch(pos.Right()))
                    {
                        SwapTileUnchecked(pos, pos.Right());
                        _hints = (pos, pos.Right());
                        return true;
                    }
                    SwapTileUnchecked(pos, pos.Right());
                }

                if (IsPosValid(pos.Top()))
                {
                    SwapTileUnchecked(pos, pos.Top());
                    if (ThreeMatch(pos) || ThreeMatch(pos.Top()))
                    {
                        SwapTileUnchecked(pos, pos.Top());
                        _hints = (pos, pos.Top());
                        return true;
                    }
                    SwapTileUnchecked(pos, pos.Top());
                }
            }
        }
        return false;
    }

    public void Reshuffle()
    {
        List<ITile> buffer = [];
        foreach (var tile in _tiles)
        {
            buffer.Add(tile);
        }

        var rand = new Random();
        for (var i = buffer.Count - 1; i >= 0; i--)
        {
            int idx = rand.Next(i + 1);

            (buffer[i], buffer[idx]) = (buffer[idx], buffer[i]);
        }

        var p = 0;
        for (int row = 0; row < Height; row++)
        {
            for (int col = 0; col < Width; col++)
            {
                _tiles[col, row] = buffer[p++];
                // _listeners.ForEach(l => l.OnCellUpdate(_tiles[col, row], (col, row)));
                // OnTileUpdate?.Invoke(_tiles[col, row], (col, row));
                MarkDirty((col, row));
            }
        }
    }

    private bool ThreeMatch(ITile matchTile, TilePos begin, List<TilePos> tiles = null)
    {
        if (matchTile == null) return false;

        bool containsBegin = false;
        List<TilePos> buffer = [];

        bool success = false;

        bool MatchInner(IEnumerable<(ITile, TilePos)> tilePass)
        {
            foreach (var (tile, pos) in tilePass)
            {
                if (ITile.IsMatch(tile, matchTile))
                {
                    buffer.Add(pos);
                    if (pos == begin)
                    {
                        containsBegin = true;
                    }
                }
                else
                {
                    if (buffer.Count >= 3 && containsBegin)
                    {
                        goto MatchDone;
                    }
                    else
                    {
                        buffer.Clear();
                        containsBegin = false;
                    }
                }
            }

            if (buffer.Count >= 3 && containsBegin)
            {
                goto MatchDone;
            }

            return false;

        MatchDone:
            buffer.Remove(begin);
            tiles?.AddRange(buffer);
            return true;
        }

        success |= MatchInner(GetCol(begin.row));

        containsBegin = false;
        buffer.Clear();

        success |= MatchInner(GetRow(begin.col));

        if (tiles != null)
        {
            if (success)
                tiles.Add(begin);
            else
                tiles.Clear();
        }

        return success;
    }

    private bool ThreeMatch(TilePos begin, List<TilePos> tiles = null) => ThreeMatch(GetTileUnchecked(begin), begin, tiles);

    public void QueueMatch(TilePos pos)
    {
        _markedTiles.Add(pos);
    }

    public ITile GetTileUnchecked(int col, int row) => _tiles[col, row];
    public ITile GetTileUnchecked(TilePos pos) => _tiles[pos.col, pos.row];

    public bool TryGetTile(int col, int row, out ITile tile)
    {
        if (IsPositionValid(col, row))
        {
            tile = _tiles[col, row];
            return true;
        }
        tile = default;
        return false;
    }

    public bool TryGetTile(TilePos pos, out ITile tile) => TryGetTile(pos.col, pos.row, out tile);

    public ITile GetTile(TilePos pos) => IsPosValid(pos) ? _tiles[pos.col, pos.row] : null;

    public void SetTile(ITile tile, int col, int row)
    {
        _tiles[col, row] = tile;
    }

    public void SetTile(ITile tile, TilePos pos) => SetTile(tile, pos.col, pos.row);

    public void PlaceTile(ITile tile, TilePos pos)
    {
        SetTile(tile, pos);
        MarkDirty(pos);
    }

    public void SwapTileUnchecked(TilePos a, TilePos b)
    {
        (_tiles[a.col, a.row], _tiles[b.col, b.row]) = (_tiles[b.col, b.row], _tiles[a.col, a.row]);
    }

    public bool TrySwapTile(TilePos a, TilePos b)
    {
        if (GetTileUnchecked(a)?.Swapable(GetTileUnchecked(b)) == false)
            return false;

        var success = false;

        SwapTileUnchecked(a, b);

        if (ThreeMatch(a))
        {
            QueueMatch(a);
            success = true;
        }

        if (ThreeMatch(b))
        {
            QueueMatch(b);
            success = true;
        }

        if (!success)
        {
            SwapTileUnchecked(a, b);
        }
        else
        {
            OnTileUpdate?.Invoke(GetTileUnchecked(a), a);
            OnTileUpdate?.Invoke(GetTileUnchecked(b), b);
            MarkDirty(a);
            MarkDirty(b);
            DoMatch();
        }

        return success;
    }

    public IEnumerable<(ITile tile, TilePos pos)> GetCol(int row)
    {
        for (int col = 0; col < Width; col++) yield return (_tiles[col, row], (col, row));
    }

    public IEnumerable<(ITile tile, TilePos pos)> GetRow(int col)
    {
        for (int row = 0; row < Height; row++) yield return (_tiles[col, row], (col, row));
    }

    public IEnumerable<(ITile, TilePos, TileSide)> GetNeighbors(TilePos pos, TileSide mask = TileSide.All)
        => Rule.NEIGHBORS
            .Select(p => (c: p.c + pos.col, r: p.r + pos.row, p.side))
            .Where(p => IsPositionValid(p.c, p.r) && mask.HasFlag(p.side))
            .Select(p => (_tiles[p.c, p.r], new TilePos(p.c, p.r), p.side));

    public IEnumerable<TilePos> GetSwapables(TilePos pos)
    {
        var tile = _tiles[pos.col, pos.row];
        return Rule.NEIGHBORS
            .Select(p => new TilePos(p.c + pos.col, p.r + pos.row))
            .Where(p => IsPosValid(p) && IsSwapable(tile, p));
    }

    public bool TryMove(TilePos from, TilePos to)
    {
        if (!IsEmpty(to)) return false;

        _tiles[to.col, to.row] = Remove(from);
        MarkDirty(to);

        foreach (var (tile, pos, side) in GetNeighbors(from, TileSide.Bottom.Exclude()))
        {
            tile?.OnNeighborRemove(this, pos, side.Oppsite());
        }

        return true;
    }

    private ITile Remove(TilePos pos)
    {
        if (IsPosValid(pos))
        {
            var t = _tiles[pos.col, pos.row];
            _tiles[pos.col, pos.row] = pos.row != 0 ? null : _respawner.Respawn(this);
            MarkDirty(pos);
            // OnTileUpdate?.Invoke(_tiles[pos.col, pos.row], pos);
            return t;
        }
        return null;
    }

    public void RemoveTilesUnchecked(IEnumerable<TilePos> positions)
    {
        foreach (var pos in positions)
        {
            if (_tiles[pos.col, pos.row] == null) continue;

            // _listeners.ForEach(l => l.OnTileRemoved(Remove(pos), pos));
            OnTileRemoved?.Invoke(Remove(pos), pos);
        }

        IEnumerable<(TilePos, TileSide)> GetSideNeighbors(TileSide side)
        {
            return positions.Select(p => p.Side(side)).Select(p => (p, side.Oppsite()));
        }

        var notifyTiles = GetSideNeighbors(TileSide.Right)
            .Concat(GetSideNeighbors(TileSide.Top))
            .Concat(GetSideNeighbors(TileSide.Left))
            .Concat(GetSideNeighbors(TileSide.Bottom))
            .Where(p => !positions.Contains(p.Item1))
            .ToArray();

        ScheduleTick(() =>
        {
            foreach (var (tile, side) in notifyTiles)
            {
                // GD.Print($"Notify {tile} {side}");
                NotifyTileRemove(tile, side);
            }
        }, 1);
    }

    public void NotifyTileRemove(TilePos pos, TileSide side)
    {
        if (GetTile(pos) is ITile tile)
        {
            tile.OnNeighborRemove(this, pos, side);
        }
    }

    // Instant emit
    public void EmitCellUpdate(ITile tile, TilePos pos)
    {
        // _listeners.ForEach(l => l.OnCellUpdate(tile, pos));
        OnTileUpdate?.Invoke(tile, pos);
        MarkDirty(pos);
    }

    public void ScheduleTick(Action action, int tick)
    {
        _scheduleBuffer.Add(new() { func = action, tick = tick });
    }

    // Tick mark
    public void MarkDirty(TilePos pos) => _dirtyTiles[pos.AsIndex(Width)] = true;

    public bool IsColValid(int col) => col >= 0 && col < Width;
    public bool IsRowValid(int row) => row >= 0 && row < Height;
    public bool IsPositionValid(int col, int row) => IsColValid(col) && IsRowValid(row);
    public bool IsPosValid(TilePos pos) => IsPositionValid(pos.col, pos.row);

    public bool IsEmpty(int col, int row) => IsPositionValid(col, row) && _tiles[col, row] == null;
    public bool IsEmpty(TilePos pos) => IsEmpty(pos.col, pos.row);

    public bool IsInteractable(TilePos pos) => _tiles[pos.col, pos.row].Interactable();
    public bool IsSwapable(ITile a, TilePos t) => a?.Swapable(_tiles[t.col, t.row]) == true;
    public bool IsSwapable(TilePos a, TilePos b) => IsSwapable(_tiles[a.col, a.row], b);
    public bool IsMatch(TilePos match, TilePos target) => _tiles[match.col, match.row].Matches(_tiles[target.col, target.row]);
}
